React + Vite のプロジェクトに Ladle を組み込んでみた
はじめに
React + Vite のプロジェクトに Ladle を組み込んでみました。
Ladle は、React の UI カタログを作成するためのツールになります。
Storybook に比べ、パフォーマンスや導入コストなどを重視したツールになっています。
詳細については以下をご確認ください。
React + Vite + Tailwind CSS のプロジェクトを使用します。 環境構築は以下の記事を参考にしてください。
環境
"react": "^18.2.0", "vite": "^5.0.0" "tailwindcss": "^3.3.6", "@ladle/react": "^4.0.2", "@playwright/test": "^1.40.1", "sync-fetch": "^0.5.2", "swr": "^2.2.4"
Ladle 環境構築
必要なパッケージをインストールします。
npm install -D @ladle/react
Ladle の設定を行います。
{ "compilerOptions": { "jsx": "react-jsx" }, "include": ["src", ".ladle"] }
"scripts": { "dev:ladle": "npx ladle serve" }
これだけで最低限の環境構築は完了です。
動作確認を行いましょう!
簡単なボタンコンポーネントを作成します。
export const Button = () => { return <button>クリック</button>; };
作成したボタンコンポーネントを表示するために、ストーリーを作成する必要があります。
import type { StoryDefault, Story } from "@ladle/react"; import { Button } from "./button"; export default { title: "Components / Button", } satisfies StoryDefault; export const DefaultButton: Story = () => <Button />;
Ladle を起動します。
npm run dev:ladle
Tailwind CSS の設定
Ladle で Tailwind CSS を使用するために、設定を行います。
Tailwind CSS 以外を設定したい場合は、下記を参考にしてください。
@tailwind base; @tailwind components; @tailwind utilities;
.ladle/components.tsx
を作成し、Tailwind CSS を読み込みます。
import "../src/index.css";
以上で設定は完了です。動作確認を行います。
npm install clsx
import clsx from "clsx"; import { FC, useMemo } from "react"; export interface ButtonProps { children: React.ReactNode; variant?: "contained" | "outlined" | "text"; } export const Button: FC<ButtonProps> = ({ children, variant = "contained", }) => { const baseClass = "rounded-md px-4 py-2"; const variantClass = useMemo(() => { switch (variant) { case "contained": return "bg-blue-500 text-white hover:bg-blue-600"; case "outlined": return "border border-blue-500 text-blue-500 hover:bg-blue-500 hover:text-white"; case "text": return "text-blue-500 hover:bg-blue-100"; } }, [variant]); return <button className={clsx(baseClass, variantClass)}>{children}</button>; };
import type { StoryDefault, Story } from "@ladle/react"; import { Button } from "./button"; export default { title: "Components / Button", } satisfies StoryDefault; export const Contained: Story = () => ( <Button variant="contained"> <p>クリック</p> </Button> ); export const Outlined: Story = () => ( <Button variant="outlined"> <p>クリック</p> </Button> ); export const Text: Story = () => ( <Button variant="text"> <p>クリック</p> </Button> );
引数の設定
Ladle では、ストーリーに引数を設定することができます。
先ほどのコードを変更してvariant
を引数に出してみましょう!
import type { StoryDefault, Story } from "@ladle/react"; import { Button } from "./button"; interface ButtonProps { variant: "contained" | "outlined" | "text"; } export default { title: "Components / Button", } satisfies StoryDefault; export const Contained: Story<ButtonProps> = ({ variant }) => ( <Button variant={variant}>クリック</Button> ); Contained.args = { variant: "contained", }; export const Outlined: Story<ButtonProps> = ({ variant }) => ( <Button variant={variant}>クリック</Button> ); Outlined.args = { variant: "outlined", }; export const Text: Story<ButtonProps> = ({ variant }) => ( <Button variant={variant}>クリック</Button> ); Text.args = { variant: "text", };
Story.bind
を使用して、テンプレートから派生したストーリーを作成し、コードの共通化をすることもできます!
import type { StoryDefault, Story } from "@ladle/react"; import { Button } from "./button"; interface ButtonProps { variant: "contained" | "outlined" | "text"; } export default { title: "Components / Button", } satisfies StoryDefault; const Template: Story<ButtonProps> = ({ variant }) => ( <Button variant={variant}>クリック</Button> ); export const Contained = Template.bind({}); Contained.args = { variant: "contained", }; export const Outlined = Template.bind({}); Outlined.args = { variant: "outlined", }; export const Text = Template.bind({}); Text.args = { variant: "text", };
ビジュアルリグレッションテスト
UI カタログを使用していると、欲しくなってくるのがビジュアルリグレッションテスト
だと思います。
ビジュアルリグレッションテスト
とは、コンポーネントの見た目が変わっていないか画像を用いて確認するテストです。
Ladle とPlaywright
を組み合わせることで実現することができます。
ビジュアルリグレッションテストをやってみましょう!
npm install -D @playwright/test sync-fetch @types/sync-fetch npx playwright install
export default { webServer: { command: "npm run dev:ladle", url: "http://localhost:61000", reuseExistingServer: true, }, };
"scripts": { "ladle:test": "playwright test", "ladle:test:update": "playwright test -u" },
import { test, expect } from "@playwright/test"; import fetch from "sync-fetch"; const url = "http://localhost:61000"; const stories = fetch(`${url}/meta.json`).json().stories; Object.keys(stories).forEach((storyKey) => { test(`${storyKey} - compare snapshots`, async ({ page }) => { await page.goto(`${url}/?story=${storyKey}&mode=preview`); await page.waitForSelector("[data-storyloaded]"); await expect(page).toHaveScreenshot(`${storyKey}.png`); }); });
以上でビジュアルリグレッションテストの環境構築は完了です。
実際に動作させてみましょう!
npm run ladle:test
最初の実行は以下のようなエラーが出ると思います。 原因は比較用の画像がないためです。
もう一度実行してみましょう。 次は成功して以下のようになると思います。
次に、コンポーネントの見た目を変更してみましょう。
Contained
コンポーネントのvariant
をoutlined
に変更します。
export const Contained = Template.bind({}); Contained.args = { variant: "outlined", };
実行すると、以下のようにエラーが出ると思います。
また、プロジェクト直下にtest-results
ディレクトリが作成されており、
その中に変更前、変更後、差分の画像が保存されていると思います。
この画像を見ることで、どのような変更があったのかを確認することができます。
コンポーネントの見た目を変更した場合、その差分が意図したものであると確認できたら、
npm run ladle:test:update
を実行して、比較用の画像を更新します。
MSW
Ladle では、MSW
を使用して API をモックすることができます。
モックを使用することでサーバーサイドを待たずに開発することができ、
またAPI のレスポンスを変更することで、レスポンスごとの表示を確認することができます。
API のモックをやってみましょう!
ladle で MSW を有効にします。
/** @type {import('@ladle/react').UserConfig} */ export default { addons: { msw: { enabled: true, }, }, };
これだけで設定は完了です。
動作確認を行います。
npm install swr
export const apiUrl = "https://example.com/api";
import { FC, useMemo } from "react"; import { apiUrl } from "../config"; import useSWR, { Fetcher } from "swr"; interface User { id: number; name: string; email: string; } const UsersPage: FC = () => { const fetcher: Fetcher<User[], string> = (url: string) => fetch(url).then((r) => r.json()); const { data: users, error } = useSWR(`${apiUrl}/users`, fetcher); const userList = useMemo(() => { if (error) return <div>データの取得に失敗しました。</div>; if (!users) return <div>Loading...</div>; return ( <> {users.map((user) => ( <li key={user.id}> <p> {user.name} / {user.email} </p> </li> ))} </> ); }, [users, error]); return ( <div> <h1 className="text-2xl">利用者一覧</h1> <ul className="m-4 list-disc">{userList}</ul> </div> ); }; export default UsersPage;
import { useSWRConfig } from "swr"; import type { StoryDefault, Story } from "@ladle/react"; import { msw } from "@ladle/react"; import { apiUrl } from "../config"; import UsersPage from "./users"; export default { title: "Pages / UsersPage", } satisfies StoryDefault; const Template: Story = () => { // mutateを使用して、SWR のキャッシュを更新させる const { mutate } = useSWRConfig(); mutate(`${apiUrl}/users`); return <UsersPage />; }; export const Default = Template.bind({}); Default.msw = [ msw.http.get(`${apiUrl}/users`, () => { return msw.HttpResponse.json([ { id: 1, name: "John", email: "john@gmail.com" }, { id: 2, name: "Bob", email: "bob@gmail.com" }, { id: 3, name: "Alice", email: "alice@gmail.com" }, ]); }), ]; export const Loading = Template.bind({}); Loading.msw = [ msw.http.get(`${apiUrl}/users`, () => { return msw.HttpResponse.json(null); }), ]; export const Error = Template.bind({}); Error.msw = [ msw.http.get(`${apiUrl}/users`, () => { return new msw.HttpResponse("Internal Server Error", { status: 500 }); }), ];
さいごに
React + Vite のプロジェクトに Ladle を組み込んでみました。
Ladle を使用することで、開発効率を上げることができると思います。
ぜひ、Ladle を使用してみてください!